6

Go编程语言:支持并发、垃圾回收的编译型系统级编程语言!Go语言从语言层面上就支持了并发,这与其他语言大不一样,不像以前我们要用Thread库 来新建线程,还要用线程安全的队列库来共享数据。

一、并发concurrency

1.基本概念

Go能处理高并发的根本原因在于执行go协程只需极少的栈内存(大概4~5KB),并且能根据需要动态增长和缩减占用的资源。

Go的高并发其实是由goroutine实现的,goroutine是由官方实现的超级“线程池”。

简单而言,goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价,我们可以很轻松的创建上万个goroutine,但它们并不是被操作系统所调度执行,而是通过系统的线程来多路派遣这些函数的执行,使得每个用go关键字执行的函数可以运行成为一个单位协程。当一个协程阻塞的时候,调度器就会自动把其他协程安排到另外的线程中去执行,从而实现了程序无等待并行化运行。而且调度的开销非常小,一颗CPU调度的规模不下于每秒百万次,这使得在程序中能够创建大量的goroutine,实现高并发的同时,依旧能保持高性能。

Goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

2.区别并发和并行

这里涉及到一个问题,很多同学搞不清楚并发与并行的区别,这里我根据我根据知乎上这个问题某位网友的例子,我觉得很好:

  • 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行
  • 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发
  • 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行

并发:你有处理多个任务的能力,不一定同时(一个CPU轮流)

并行:有同时处理多个任务的能力(多个CPU同时)

并发和并行都可以是很多个线程,就看这些线程能不能同时被(多个)CPU执行,可以说明是并行,并发是多个线程被一个CPU轮流切换着执行。

并发不是并行:Concurrency Is Not Parallelism
并发主要由切换时间片来实现“同时”运行,而并行则是直接利用多核实现多线程的运行,但Go可以设置使用核数,以发挥多核计算机的能力。

并发时,由于cpu执行的太快了,不停地来回切换,让人以为是同时进行。
并发比并行更优秀,充分的利用了CPU。

3.信道Channel

  • Channelgoroutine 沟通的桥梁,大都是阻塞同步的
  • 通过 make 创建,close 关闭
  • Channel 是引用类型
  • 可以使用 for range 来迭代不断操作channel
  • 可以设置单向或双向通道
  • 可以设置缓存大小,在未被填满前不会发生阻塞

4.Select

  • 可处理一个或多个channel的发送与接收
  • 同时有多个可用的channel时按随机排序处理
  • 可用空的 select 来阻塞 main 函数
  • 可设置超时

二、并发示例

1.未使用channel关键字

package main


import (
    "fmt"
    "time"
)  

func main() {
    go Go()      // 使用go关键字就可执行goroutine
    time.Sleep(2 * time.Second)
    fmt.Println("Hey ...!")
}

func Go() {
    fmt.Println("Go ...!")
    
}

打印:

➜  src go run myfirstgo/concurrency.go
Go ...!
Hey ...!

2.使用channel关键字

package main


import (
    "fmt"
)  

func main() {

    // 创建channel
    c := make(chan bool)

    // 执行到此处会阻塞
    go func() {
        fmt.Println("Go ...!")
        c <- true
    }()

    // 将true的东西存到channel当中,然后读出来结束运行,即通知main函数,我这里运行完成了,可以结束了
    <-c
    
    fmt.Println("Hey ...!")
}

打印:

➜  src go run myfirstgo/concurrency.go
Go ...!
Hey ...!

3.range示例

func main() {

    // 创建channel
    c := make(chan bool)

    // 执行到此处会阻塞
    go func() {
        fmt.Println("Go ...!")
        c <- true
        close(c)
    }()

   for v := range c {
         fmt.Println(v)
   }
    
    fmt.Println("Hey ...!")
}

打印:

➜  src go run myfirstgo/concurrency.go
Go ...!
true
Hey ...!

4.多核

package main


import (
    "fmt"
    "runtime"
)  

// 当使用单线程执行时,会按部就班,按照顺序1,2,3,4执行下去
// 当使用多个CPU核数时,任务分配是不定的,

func main() {

    // 使用多核
    runtime.GOMAXPROCS(runtime.NumCPU())
    c := make(chan bool, 10)
    for i := 0; i < 10; i++ {
        go Go(c, i)
    }
    <-c
}

func Go(c chan bool, index int) {
    a := 1
    for i := 0; i < 100000000; i++ {
        a += i
    }

    fmt.Println(index, a)

    if index == 9 {
        c <- true
    }
}

运行多次的执行结果:

➜  src go run myfirstgo/concurrency.go
3 4999999950000001
0 4999999950000001
1 4999999950000001
9 4999999950000001
➜  src go run myfirstgo/concurrency.go
2 4999999950000001
1 4999999950000001
3 4999999950000001
9 4999999950000001
➜  src go run myfirstgo/concurrency.go
9 4999999950000001
➜  src go run myfirstgo/concurrency.go
2 4999999950000001
9 4999999950000001

当使用多个CPU核数(runtime.GOMAXPROCS)时,任务分配是不定的,所以会出现上边的结果。

这里有两种解决方案:
第一种:
设置一个缓存长度为10的channel

package main


import (
    "fmt"
    "runtime"
)  

// 当使用单线程执行时,会按部就班,按照顺序1,2,3,4执行下去
// 当使用多个CPU核数时,任务分配是不定的,

func main() {

    // 使用多核
    runtime.GOMAXPROCS(runtime.NumCPU())
    c := make(chan bool, 10)
    for i := 0; i < 10; i++ {
        go Go(c, i)
    }

    // 设置一个缓存长度为10 的channel
    for i := 0; i < 10; i++ {
        <-c
    }

    
}

func Go(c chan bool, index int) {
    a := 1
    for i := 0; i < 100000000; i++ {
        a += i
    }

    fmt.Println(index, a)
    c <- true

}

打印:

➜  src go run myfirstgo/concurrency.go
1 4999999950000001
2 4999999950000001
9 4999999950000001
3 4999999950000001
7 4999999950000001
0 4999999950000001
8 4999999950000001
6 4999999950000001
4 4999999950000001
5 4999999950000001
➜  src go run myfirstgo/concurrency.go
0 4999999950000001
9 4999999950000001
5 4999999950000001
1 4999999950000001
7 4999999950000001
2 4999999950000001
8 4999999950000001
3 4999999950000001
6 4999999950000001
4 4999999950000001

第二种:
不是通过channel解决的,而是通过sync包解决的,它有一个waitGroup,可以创建一个任务组,可以添加任务,每完成一个任务,就标记完成Done,main函数的主要作用就是判断是否所有的任务都完成了,如果都完成了,则退出程序。

package main

import (
    "fmt"
    "runtime"
    "sync"
)  

// 当使用单线程执行时,会按部就班,按照顺序1,2,3,4执行下去
// 当使用多个CPU核数时,任务分配是不定的,
func main() {

    // 使用多核
    runtime.GOMAXPROCS(runtime.NumCPU())

    // sync 
    wg := sync.WaitGroup{}
    wg.Add(10)

    for i := 0; i < 10; i++ {
        go Go(&wg, i)
    }

    wg.Wait()

    
}

func Go(wg *sync.WaitGroup, index int) {
    a := 1
    for i := 0; i < 100000000; i++ {
        a += i
    }

    fmt.Println(index, a)
    
    wg.Done()
}

打印:

➜  src go run myfirstgo/concurrency.go
1 4999999950000001
5 4999999950000001
7 4999999950000001
9 4999999950000001
6 4999999950000001
8 4999999950000001
0 4999999950000001
2 4999999950000001
3 4999999950000001
4 4999999950000001
➜  src go run myfirstgo/concurrency.go
1 4999999950000001
9 4999999950000001
2 4999999950000001
3 4999999950000001
4 4999999950000001
5 4999999950000001
0 4999999950000001
6 4999999950000001
7 4999999950000001
8 4999999950000001

相关文章:
golang语言并发与并行——goroutine和channel的详细理解(一)
golang的goroutine调度机制


Corwien
6.3k 声望1.6k 粉丝

为者常成,行者常至。